 /*******************************************************************************
  * Copyright (c) 2004, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.dnd;

 import java.util.ArrayList ;
 import java.util.Iterator ;
 import java.util.List ;

 import org.eclipse.jface.util.Geometry;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Tracker;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.internal.DragCursors;

 /**
  * Provides the methods for attaching drag-and-drop listeners to SWT controls.
  */
 public class DragUtil {
     private static final String DROP_TARGET_ID = "org.eclipse.ui.internal.dnd.dropTarget"; //$NON-NLS-1$

     /**
      * The location where all drags will end. If this is non-null, then
      * all user input is ignored in drag/drop. If null, we use user input
      * to determine where objects should be dropped.
      */
     private static TestDropLocation forcedDropTarget = null;
     
     /**
      * List of IDragOverListener
      */
     private static List defaultTargets = new ArrayList ();

     /**
      * Sets the drop target for the given control. It is possible to add one or more
      * targets for a "null" control. This becomes a default target that is used if no
      * other targets are found (for example, when dragging objects off the application
      * window).
      *
      * @param control the control that should be treated as a drag target, or null
      * to indicate the default target
      * @param target the drag target to handle the given control
      */
     public static void addDragTarget(Control control, IDragOverListener target) {
         if (control == null) {
             defaultTargets.add(target);
         } else {
             List targetList = getTargetList(control);

             if (targetList == null) {
                 targetList = new ArrayList (1);
             }
             targetList.add(target);
             control.setData(DROP_TARGET_ID, targetList);
         }
     }

     /**
      * Return the list of 'IDragOverListener' elements associated with
      * the given control. If there's a 'global' listener then always
      * return it.
      *
      * @param control
      * @return
      */
     private static List getTargetList(Control control) {
         List result = (List ) control.getData(DROP_TARGET_ID);
         return result;
     }

     /**
      * Removes a drop target from the given control.
      *
      * @param control
      * @param target
      */
     public static void removeDragTarget(Control control,
             IDragOverListener target) {
         if (control == null) {
             defaultTargets.remove(target);
         } else {
             List targetList = getTargetList(control);
             if (targetList != null) {
                 targetList.remove(target);
                 if (targetList.isEmpty()) {
                     control.setData(DROP_TARGET_ID, null);
                 }
             }
         }
     }

     /**
      * Shorthand method. Returns the bounding rectangle for the given control, in
      * display coordinates. Note that all 'Shell' controls are expected to be 'top level'
      * so DO NOT do the origin offset for them.
      *
      * @param draggedItem
      * @param boundsControl
      * @return
      */
     public static Rectangle getDisplayBounds(Control boundsControl) {
         Control parent = boundsControl.getParent();
         if (parent == null || boundsControl instanceof Shell) {
             return boundsControl.getBounds();
         }

         return Geometry.toDisplay(parent, boundsControl.getBounds());
     }

     public static boolean performDrag(final Object draggedItem,
             Rectangle sourceBounds, Point initialLocation, boolean allowSnapping) {

         IDropTarget target = dragToTarget(draggedItem, sourceBounds,
                 initialLocation, allowSnapping);

         if (target == null) {
             return false;
         }

         target.drop();

         // If the target can handle a 'finished' notification then send one
 if (target!= null && target instanceof IDropTarget2) {
             ((IDropTarget2)target).dragFinished(true);
         }

         return true;
     }

     /**
      * Drags the given item to the given location (in display coordinates). This
      * method is intended for use by test suites.
      *
      * @param draggedItem object being dragged
      * @param finalLocation location being dragged to
      * @return true iff the drop was accepted
      */
     public static boolean dragTo(Display display, Object draggedItem,
             Point finalLocation, Rectangle dragRectangle) {
         Control currentControl = SwtUtil.findControl(display, finalLocation);

         IDropTarget target = getDropTarget(currentControl, draggedItem,
                 finalLocation, dragRectangle);

         if (target == null) {
             return false;
         }

         target.drop();

         return true;
     }

     /**
      * Forces all drags to end at the given position (display coordinates). Intended
      * for use by test suites. If this method is called, then all subsequent calls
      * to performDrag will terminate immediately and behave as though the object were
      * dragged to the given location. Calling this method with null cancels this
      * behavior and causes performDrag to behave normally.
      *
      * @param forcedLocation location where objects will be dropped (or null to
      * cause drag/drop to behave normally).
      */
     public static void forceDropLocation(TestDropLocation forcedLocation) {
         forcedDropTarget = forcedLocation;
     }
     
     /**
      * Drags the given item, given an initial bounding rectangle in display coordinates.
      * Due to a quirk in the Tracker class, changing the tracking rectangle when using the
      * keyboard will also cause the mouse cursor to move. Since "snapping" causes the tracking
      * rectangle to change based on the position of the mouse cursor, it is impossible to do
      * drag-and-drop with the keyboard when snapping is enabled.
      *
      * @param draggedItem object being dragged
      * @param sourceBounds initial bounding rectangle for the dragged item
      * @param initialLocation initial position of the mouse cursor
      * @param allowSnapping true iff the rectangle should snap to the drop location. This must
      * be false if the user might be doing drag-and-drop using the keyboard.
      *
      * @return
      */
     static IDropTarget dragToTarget(final Object draggedItem,
             final Rectangle sourceBounds, final Point initialLocation,
             final boolean allowSnapping) {
         final Display display = Display.getCurrent();

         // Testing...immediately 'drop' onto the test target
 if (forcedDropTarget != null) {
             Point location = forcedDropTarget.getLocation();

             Control currentControl = SwtUtil.findControl(forcedDropTarget.getShells(), location);
             return getDropTarget(currentControl, draggedItem, location,
                     sourceBounds);
         }

         // Create a tracker. This is just an XOR rect on the screen.
 // As it moves we notify the drag listeners.
 final Tracker tracker = new Tracker(display, SWT.NULL);
         tracker.setStippled(true);

         tracker.addListener(SWT.Move, new Listener() {
             public void handleEvent(final Event event) {
                 display.syncExec(new Runnable () {
                     public void run() {
                         // Get the curslor location as a point
 Point location = new Point(event.x, event.y);

                         // Select a drop target; use the global one by default
 IDropTarget target = null;
                             
                         Control targetControl = display.getCursorControl();

                         // Get the drop target for this location
 target = getDropTarget(targetControl,
                                 draggedItem, location,
                                 tracker.getRectangles()[0]);

                         // Set up the tracker feedback based on the target
 Rectangle snapTarget = null;
                         if (target != null) {
                             snapTarget = target.getSnapRectangle();

                             tracker.setCursor(target.getCursor());
                         } else {
                             tracker.setCursor(DragCursors
                                     .getCursor(DragCursors.INVALID));
                         }

                         // If snapping then reset the tracker's rectangle based on the current drop target
 if (allowSnapping) {
                             if (snapTarget == null) {
                                 snapTarget = new Rectangle(sourceBounds.x
                                         + location.x - initialLocation.x,
                                         sourceBounds.y + location.y
                                                 - initialLocation.y,
                                         sourceBounds.width, sourceBounds.height);
                             }

                             // Try to prevent flicker: don't change the rectangles if they're already in
 // the right location
 Rectangle[] currentRectangles = tracker.getRectangles();

                             if (!(currentRectangles.length == 1 && currentRectangles[0]
                                     .equals(snapTarget))) {
                                 tracker.setRectangles(new Rectangle[] { snapTarget });
                             }
                         }
                     }
                 });
             }
         });

         // Setup...when the drag starts we might already be over a valid target, check this...
 // If there is a 'global' target then skip the check
 IDropTarget target = null;
         Control startControl = display.getCursorControl();
         
         if (startControl != null && allowSnapping) {
             target = getDropTarget(startControl,
                 draggedItem, initialLocation,
                 sourceBounds);
         }

         // Set up an initial tracker rectangle
 Rectangle startRect = sourceBounds;
         if (target != null) {
             Rectangle rect = target.getSnapRectangle();
             
             if (rect != null) {
                 startRect = rect;
             }

             tracker.setCursor(target.getCursor());
         }
         
         if (startRect != null) {
             tracker.setRectangles(new Rectangle[] { Geometry.copy(startRect)});
         }

         // Tracking Loop...tracking is preformed on the 'SWT.Move' listener registered
 // against the tracker.

         // HACK:
 // Some control needs to capture the mouse during the drag or other
 // controls will interfere with the cursor
 Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
         if (shell != null) {
             shell.setCapture(true);
         }

         // Run tracker until mouse up occurs or escape key pressed.
 boolean trackingOk = tracker.open();

         // HACK:
 // Release the mouse now
 if (shell != null) {
             shell.setCapture(false);
         }

         // Done tracking...

         // Get the current drop target
 IDropTarget dropTarget = null;
         Point finalLocation = display.getCursorLocation();
         Control targetControl = display.getCursorControl();
         dropTarget = getDropTarget(targetControl, draggedItem,
                 finalLocation, tracker.getRectangles()[0]);

         // Cleanup...
 tracker.dispose();
         
         // if we're going to perform a 'drop' then delay the issuing of the 'finished'
 // callback until after it's done...
 if (trackingOk) {
             return dropTarget;
         }
         else if (dropTarget!= null && dropTarget instanceof IDropTarget2) {
             // If the target can handle a 'finished' notification then send one
 ((IDropTarget2)dropTarget).dragFinished(false);
         }
         
         return null;
     }
     
     /**
      * Given a list of IDragOverListeners and a description of what is being dragged, it returns
      * a IDropTarget for the current drop.
      *
      * @param toSearch
      * @param mostSpecificControl
      * @param draggedObject
      * @param position
      * @param dragRectangle
      * @return
      */
     private static IDropTarget getDropTarget(List toSearch,
             Control mostSpecificControl, Object draggedObject, Point position,
             Rectangle dragRectangle) {
         if (toSearch == null) {
             return null;
         }

         Iterator iter = toSearch.iterator();
         while (iter.hasNext()) {
             IDragOverListener next = (IDragOverListener) iter.next();

             IDropTarget dropTarget = next.drag(mostSpecificControl,
                     draggedObject, position, dragRectangle);

             if (dropTarget != null) {
                 return dropTarget;
             }
         }

         return null;
     }

     /**
      * Returns the drag target for the given control or null if none.
      *
      * @param toSearch
      * @param e
      * @return
      */
     public static IDropTarget getDropTarget(Control toSearch,
             Object draggedObject, Point position, Rectangle dragRectangle) {
         // Search for a listener by walking the control's parent hierarchy
 for (Control current = toSearch; current != null; current = current
                 .getParent()) {
             IDropTarget dropTarget = getDropTarget(getTargetList(current),
                     toSearch, draggedObject, position, dragRectangle);

             if (dropTarget != null) {
                 return dropTarget;
             }

             // Don't look to parent shells for drop targets
 if (current instanceof Shell) {
                 break;
             }
         }

         // No controls could handle this event -- check for default targets
 return getDropTarget(defaultTargets, toSearch, draggedObject, position,
                 dragRectangle);
     }

     /**
      * Returns the location of the given event, in display coordinates
      * @return
      */
     public static Point getEventLoc(Event event) {
         Control ctrl = (Control) event.widget;
         return ctrl.toDisplay(new Point(event.x, event.y));
     }

 }

